Normal Transformation

In the last section, we saw that our computation of the cosine of the angle of incidence has certain requirements. Namely, that the two vectors involved, the surface normal and the light direction, are of unit length. The light direction can be assumed to be of unit length, since it is passed directly as a uniform.

The surface normal can also be assumed to be of unit length. Initially. However, the normal undergoes a transformation by an arbitrary matrix; there is no guarantee that this transformation will not apply scaling or other transformations to the vector that will result in a non-unit vector.

Of course, it is easy enough to correct this. The GLSL function normalize will return a vector that is of unit length without changing the direction of the input vector.

And while mathematically this would function, geometrically, it would be nonsense. For example, consider a 2D circle. We can apply a non-uniform scale (different scales in different axes) to the positions on this circle that will transform it into an ellipse:

Figure 9.7. Circle Scaling

Circle Scaling

This is all well and good, but consider the normals in this transformation:

Figure 9.8. Circle Scaling with Normals

Circle Scaling with Normals

The ellipse in the middle has the normals that you would expect if you transformed the normals from the circle by the same matrix the circle was transformed by. They may be unit length, but they no longer reflect the shape of the ellipse. The ellipse on the right has normals that reflect the actual shape.

It turns out that, what you really want to do is transform the normals with the same rotations as the positions, but invert the scales. That is, a scale of 0.5 along the X axis will shrink positions in that axis by half. For the surface normals, you want to double the X value of the normals, then normalize the result.

This is easy if you have a simple matrix. But more complicated matrices, composed from multiple successive rotations, scales, and other operations, are not so easy to compute.

Instead, what we must do is compute something called the inverse transpose of the matrix in question. This means we first compute the inverse matrix, then compute the transpose of that matrix. The transpose of a matrix is simply the same matrix flipped along the diagonal. The columns of the original matrix are the rows of its transpose. That is:

Equation 9.4. Matrix Transpose


So how does this inverse transpose help us?

Remember: what we want is to invert the scales of our matrix without affecting the rotational characteristics of our matrix. Given a 3x3 matrix M that is composed of only rotation and scale transformations, we can re-express this matrix as follows:

That is, the matrix can be expressed as doing a rotation into a space, followed by a single scale transformation, followed by another rotation. We can do this regardless of how many scale and rotation matrices were used to build M. That is, M could be the result of twenty rotation and scale matrices, but all of those can be extracted into two rotations with a scale inbetween.[5]

Recall that what we want to do is invert the scales in our transformation. Where we scale by 0.4 in the original, we want to scale by 2.5 in the inverse. The inverse matrix of a pure scale matrix is a matrix with each of the scaling components inverted. Therefore, we can express the matrix that we actually want as this:

An interesting fact about pure-rotation matrices: the inverse of any rotation matrix is its transpose. Also, taking the inverse of a matrix twice results in the original matrix. Therefore, you can express any pure-rotation matrix as the inverse transpose of itself, without affecting the matrix. Since the inverse is its transpose, and doing a transpose twice on a matrix does not change its value, the inverse-transpose of a rotation matrix is a no-op.

Also, since the values in pure-scale matrices are along the diagonal, a transpose operation on scale matrices does nothing. With these two facts in hand, we can re-express the matrix we want to compute as:

Using matrix algebra, we can factor the transposes out, but doing so requires reversing the order of the matrix multiplication:

Similar, we can factor out the inverse operations, but this requires reversing the order again:

Thus, the inverse-transpose solves our problem. And both GLM and GLSL have nice functions that can do these operations for us. Though really, if you can avoid doing an inverse-transpose in GLSL, you are strongly advised to do so; this is not a trivial computation.

We do this in the Scale and Lighting tutorial. It controls mostly the same as the previous tutorial, with a few exceptions. Pressing the space bar will toggle between a regular cylinder and a scaled one. The T key will toggle between properly using the inverse-transpose (the default) and not using the inverse transpose. The rendering code for the cylinder is as follows:

Example 9.5. Lighting with Proper Normal Transform

glutil::PushStack push(modelMatrix);

modelMatrix.ApplyMatrix(g_objtPole.CalcMatrix());

if(g_bScaleCyl)
{
    modelMatrix.Scale(1.0f, 1.0f, 0.2f);
}

glUseProgram(g_VertexDiffuseColor.theProgram);
glUniformMatrix4fv(g_VertexDiffuseColor.modelToCameraMatrixUnif, 1, GL_FALSE, glm::value_ptr(modelMatrix.Top()));
glm::mat3 normMatrix(modelMatrix.Top());
if(g_bDoInvTranspose)
{
    normMatrix = glm::transpose(glm::inverse(normMatrix));
}
glUniformMatrix3fv(g_VertexDiffuseColor.normalModelToCameraMatrixUnif, 1, GL_FALSE, glm::value_ptr(normMatrix));
glUniform4f(g_VertexDiffuseColor.lightIntensityUnif, 1.0f, 1.0f, 1.0f, 1.0f);
g_pCylinderMesh->Render("lit-color");
glUseProgram(0);

It's pretty self-explanatory.

Figure 9.9. Lighting and Scale

Lighting and Scale

One more thing to note before we move on. Doing the inverse-transpose is only really necessary if you are using a non-uniform scale. In practice, it's actually somewhat rare to use this kind of scale factor. We do it in these tutorials, so that it is easier to build models from simple geometric components. But when you have an actual modeller creating objects for a specific purpose, non-uniform scales generally are not used. At least, not in the output mesh. It's better to just get the modeller to adjust the model as needed in their modelling application.

Uniform scales are more commonly used. So you still need to normalize the normal after transforming it with the model-to-camera matrix, even if you are not using the inverse-transpose.



[5] We will skip over deriving how exactly this is true. If you are interested, search for Singular Value Decomposition. But be warned: it is math-heavy.